﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;

using NUnit.Framework;

namespace UiAutomationExtensionTest
{
    #region AssertExtension
    /// <summary>
    /// <see cref="NUnit.Framework.Assert"/> をメソッドチェーンで書くための拡張メソッド群。
    /// </summary>
    /// <remarks>
    /// <see cref="NUnit.Framework.Assert"/> をメソッドチェーンで書くための拡張メソッド群。
    /// </remarks>
    public static class AssertExtension
    {
        #region Is
        /// <summary>
        /// ２つの値が等しいかどうかテストする。
        /// </summary>
        /// <typeparam name="T">任意の型。</typeparam>
        /// <param name="actual">テストされる値。</param>
        /// <param name="expected">期待される値。</param>
        /// <remarks>
        /// <see cref="NUnit.Framework.Assert.AreEqual(object,object)"/> を実行する拡張メソッド。
        /// </remarks>
        public static void Is<T>(this T actual, T expected)
        {
            Assert.AreEqual(expected, actual);
        }
        #endregion

        #region IsNot
        /// <summary>
        /// ２つの値が等しくないことをテストする。
        /// </summary>
        /// <typeparam name="T">任意の型。</typeparam>
        /// <param name="actual">テストされる値。</param>
        /// <param name="expected">期待されない値。</param>
        /// <remarks>
        /// <see cref="NUnit.Framework.Assert.AreNotEqual(object,object)"/> を実行する拡張メソッド。
        /// </remarks>
        public static void IsNot<T>(this T actual, T expected)
        {
            Assert.AreNotEqual(expected, actual);
        }
        #endregion

        #region IsNull
        /// <summary>
        /// 値が null であることをテストする。
        /// </summary>
        /// <typeparam name="T">任意の型。</typeparam>
        /// <param name="value">テストされる値。</param>
        /// <remarks>
        /// <see cref="NUnit.Framework.Assert.IsNull(object)"/> を実行する拡張メソッド。
        /// </remarks>
        public static void IsNull<T>(this T value)
        {
            Assert.IsNull(value);
        }
        #endregion

        #region IsNotNull
        /// <summary>
        /// 値が null でないことをテストする。
        /// </summary>
        /// <typeparam name="T">任意の型。</typeparam>
        /// <param name="value">テストされる値。</param>
        /// <remarks>
        /// <see cref="NUnit.Framework.Assert.IsNotNull(object)"/> を実行する拡張メソッド。
        /// </remarks>
        public static void IsNotNull<T>(this T value)
        {
            Assert.IsNotNull(value);
        }
        #endregion

        #region IsEmpty
        /// <summary>
        /// コレクションが空であることをテストする。
        /// </summary>
        /// <typeparam name="T">任意のコレクション。</typeparam>
        /// <param name="value">テストされる値。</param>
        /// <remarks>
        /// <see cref="NUnit.Framework.Assert.IsEmpty(System.Collections.IEnumerable)"/> を実行する拡張メソッド。
        /// </remarks>
        public static void IsEmpty<T>(this T value)
            where T : System.Collections.IEnumerable
        {
            Assert.IsEmpty(value);
        }
        #endregion

        #region AsDynamic

        #region AsDynamic

        /// <summary>
        /// オブジェクトの private メンバにアクセスするためのアクセサを取得する。
        /// </summary>
        /// <typeparam name="T">任意の型。</typeparam>
        /// <param name="target">任意のオブジェクト。</param>
        /// <returns>private メンバにアクセスするための <see cref="System.Dynamic.DynamicObject"/>。</returns>
        /// <remarks>
        /// オブジェクトの private メンバにアクセスするためのアクセサを取得する。
        /// </remarks>
        public static dynamic AsDynamic<T>(this T target)
        {
            return new DynamicAccessor(target, target.GetType());
        }

        /// <summary>
        /// オブジェクトの private メンバにアクセスするためのアクセサを取得する。
        /// </summary>
        /// <param name="target">任意のオブジェクト。</param>
        /// <param name="type">オブジェクトの型。</param>
        /// <returns>private メンバにアクセスするための <see cref="System.Dynamic.DynamicObject"/>。</returns>
        /// <remarks>
        /// オブジェクトの型を指定して private メンバにアクセスするためのアクセサを取得する。
        /// </remarks>
        public static dynamic AsDynamic(this object target, Type type)
        {
            if (!type.IsAssignableFrom(target.GetType()))
                throw new InvalidCastException(string.Format("Type <{0}> cannot assign to <{1}>", target.GetType(), type));

            return new DynamicAccessor(target, type);
        }

        #endregion

        #region CreateDynamic
        /// <summary>
        /// 指定した型のインスタンスを生成し、private メンバにアクセスするためのアクセサを取得する。
        /// </summary>
        /// <param name="type">任意の型。</param>
        /// <param name="args">コンストラクタの引数。</param>
        /// <returns>private メンバにアクセスするための <see cref="System.Dynamic.DynamicObject"/>。</returns>
        /// <remarks>
        /// 指定した型からインスタンスを生成し、生成したインスタンスへアクセスするための
        /// アクセサを取得する。<br />
        /// private クラスへのアクセスなどに使用できる。
        /// </remarks>
        public static dynamic CreateDynamic(this Type type, params object[] args)
        {
            var target = CreateInstance(type, args);
            return new DynamicAccessor(target, type);
        }
        #endregion

        #region CreateInstance
        /// <summary>
        /// 指定した型のインスタンスを取得する。
        /// </summary>
        /// <param name="type">任意の型。</param>
        /// <param name="args">コンストラクタの引数。</param>
        /// <returns>指定した型のインスタンス。</returns>
        /// <remarks>
        /// public なコンストラクタを持たないクラスに対してインスタンスを取得する。
        /// </remarks>
        public static object CreateInstance(this Type type, params object[] args)
        {
            var flags = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.OptionalParamBinding;
            var obj = Activator.CreateInstance(type, flags, null, args, null);
            if (obj == null)
                throw new ArgumentException(string.Format("constructor with parameters \"{0}\" not found in {1}", args, type));

            return obj;
        }
        #endregion

        #region ToDynamic
        /// <summary>
        /// 指定した型の static な private メンバにアクセスするためのアクセサを取得する。
        /// </summary>
        /// <param name="type">メソッドを所有するクラスの型。</param>
        /// <returns>private メンバにアクセスするための <see cref="System.Dynamic.DynamicObject"/>。</returns>
        /// <remarks>
        /// 指定した型の static な private メンバにアクセスするためのアクセサを取得する。
        /// </remarks>
        public static dynamic ToDynamic(this Type type)
        {
            return new DynamicAccessor(type);
        }
        #endregion

        #region AsStatic
        /// <summary>
        /// 指定した型の static な private メンバにアクセスするためのアクセサを取得する。
        /// </summary>
        /// <param name="type">メソッドを所有するクラスの型。</param>
        /// <returns>private メンバにアクセスするための <see cref="System.Dynamic.DynamicObject"/>。</returns>
        /// <remarks>
        /// 指定した型の static な private メンバにアクセスするためのアクセサを取得する。
        /// </remarks>
        [Obsolete]
        public static dynamic AsStatic(this Type type)
        {
            return new DynamicAccessor(type);
        }
        #endregion

        #region DynamicAccessor
        /// <summary>
        /// private メンバにアクセスするためのアクセサ。
        /// </summary>
        private class DynamicAccessor : DynamicObject
        {
            #region _ctor

            /// <summary>
            /// 指定したメンバを呼び出す対象となるオブジェクト。
            /// </summary>
            private readonly object target;

            /// <summary>
            /// <see cref="target"/> の型。
            /// </summary>
            private readonly Type type;

            /// <summary>
            /// メンバの検索方法を指定する <see cref="System.Reflection.BindingFlags"/> のビットマスク。
            /// </summary>
            private readonly BindingFlags TransparentFlags;

            /// <summary>
            /// <see cref="DynamicAccessor"/> の新しいインスタンスを生成する。
            /// </summary>
            /// <param name="type">メンバを検索する型。</param>
            /// <remarks>
            /// Static メンバのアクセス用に <see cref="DynamicAccessor"/> の
            /// 新しいインスタンスを生成する。
            /// </remarks>
            public DynamicAccessor(Type type)
            {
                this.target = null;
                this.type = type;
                this.TransparentFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
            }

            /// <summary>
            /// <see cref="DynamicAccessor"/> の新しいインスタンスを生成する。
            /// </summary>
            /// <param name="target">指定したメンバを呼び出す対象となるオブジェクト。</param>
            /// <param name="type">オブジェクトの型。</param>
            /// <remarks>
            /// インスタンスメンバのアクセス用に <see cref="DynamicAccessor"/> の
            /// 新しいインスタンスを生成する。
            /// </remarks>
            public DynamicAccessor(object target, Type type)
            {
                this.target = target;
                this.type = type;
                this.TransparentFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            }

            #endregion

            #region Indexer

            /// <summary>
            /// インデクサから値を取得する。
            /// </summary>
            /// <param name="binder">演算に関係する情報を提供する。</param>
            /// <param name="indexes">演算に使用されるインデックス。</param>
            /// <param name="result">インデクサから取得した値。</param>
            /// <returns>操作が正常に終了した場合は true、そうでない場合は false。</returns>
            public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
            {
                try
                {
                    result = type.InvokeMember("Item", this.TransparentFlags | BindingFlags.GetProperty, null, this.target, indexes);
                    return true;
                }
                catch (MissingMethodException)
                {
                    throw new ArgumentException(string.Format("indexer not found : Type <{0}>", this.type.Name));
                }
            }

            /// <summary>
            /// インデクサへの値を設定する。
            /// </summary>
            /// <param name="binder">演算に関係する情報を提供する。</param>
            /// <param name="indexes">演算に使用されるインデックス。</param>
            /// <param name="value">インデクサに設定する値。</param>
            /// <returns>操作が正常に終了した場合は true、そうでない場合は false。</returns>
            public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
            {
                try
                {
                    type.InvokeMember("Item", this.TransparentFlags | BindingFlags.SetProperty, null, this.target, indexes.Concat(new[] { value }).ToArray());
                    return true;
                }
                catch (MissingMethodException)
                {
                    throw new ArgumentException(string.Format("indexer not found : Type <{0}>", this.type.Name));
                }
            }

            #endregion

            #region Field / Property

            /// <summary>
            /// フィールド、およびプロパティからお値を取得する。
            /// </summary>
            /// <param name="binder">演算を呼び出したオブジェクトに関する情報を提供する。</param>
            /// <param name="result">フィールド、およびプロパティから取得した値。</param>
            /// <returns>操作が正常に終了した場合は true、そうでない場合は false。</returns>
            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                var accessor = new ReflectAccessor(this.target, this.type, binder.Name, this.TransparentFlags);
                result = accessor.GetValue();
                return true;
            }

            /// <summary>
            /// フィールド、およびプロパティに値を設定する。
            /// </summary>
            /// <param name="binder">演算を呼び出したオブジェクトに関する情報を提供する。</param>
            /// <param name="value">フィールド、およびプロパティに設定する値。</param>
            /// <returns>操作が正常に終了した場合は true、そうでない場合は false。</returns>
            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                var accessor = new ReflectAccessor(this.target, this.type, binder.Name, this.TransparentFlags);
                accessor.SetValue(value);
                return true;
            }

            #region ReflectAccessor
            /// <summary>
            /// リフレクションを利用してフィールド、およびプロパティへのアクセスを行なうためのアクセサ。
            /// </summary>
            private class ReflectAccessor
            {
                /// <summary>
                /// フィールド、およびプロパティから値を取得する。
                /// </summary>
                public Func<object> GetValue { get; private set; }

                /// <summary>
                /// フィールド、およびプロパティへ値を設定する。
                /// </summary>
                public Action<object> SetValue { get; private set; }

                /// <summary>
                /// <see cref="ReflectAccessor"/> の新しいインスタンスを生成する。
                /// </summary>
                /// <param name="target">メンバを呼び出す対象となるオブジェクト。</param>
                /// <param name="type">オブジェクトの型。</param>
                /// <param name="name">呼び出すメンバ名。</param>
                /// <param name="flags">メンバの検索方法を指定する <see cref="System.Reflection.BindingFlags"/> のビットマスク。</param>
                public ReflectAccessor(object target, Type type, string name, BindingFlags flags)
                {
                    var field = type.GetField(name, flags);
                    if (field != null)
                    {
                        this.GetValue = () => field.GetValue(target);
                        this.SetValue = value => field.SetValue(target, value);
                        return;
                    }

                    var property = type.GetProperty(name, flags);
                    if (property != null)
                    {
                        this.GetValue = () => property.GetValue(target, null);
                        this.SetValue = value => property.SetValue(target, value, null);
                        return;
                    }

                    throw new ArgumentException(string.Format("\"{0}\" not found : Type <{1}>", name, type.Name));
                }
            }
            #endregion

            #endregion

            #region Method

            /// <summary>
            /// メソッドを呼び出す。
            /// </summary>
            /// <param name="binder">演算に関する情報を提供する。</param>
            /// <param name="args">メソッドに渡す引数。</param>
            /// <param name="result">メソッドの戻り値。</param>
            /// <returns>操作が正常に終了した場合は true、そうでない場合は false。</returns>
            public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
            {
                var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
                if (csharpBinder == null)
                    throw new ArgumentException("is not c# code");

                var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>).ToArray();
                var paramTypes = (binder.GetType().GetField("Cache", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(binder) as Dictionary<Type, object>)
                    .First()
                    .Key
                    .GetGenericArguments()
                    .Skip(2)
                    .Take(args.Length)
                    .ToArray();

                var method = this.MatchMethod(binder.Name, args, typeArgs, paramTypes);
                if (method.GetParameters().Length > 0 && method.GetParameters().Last().IsDefined(typeof(ParamArrayAttribute), false))
                {
                    var flags = BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.OptionalParamBinding;
                    result = this.type.InvokeMember(binder.Name, flags, null, this.target, args);
                }
                else
                {
                    result = method.Invoke(this.target, args);
                }

                return true;
            }

            #region MatchMethod

            /// <summary>
            /// 指定した条件に一致するメソッドの <see cref="System.Reflection.MethodInfo"/> を取得する。
            /// </summary>
            /// <param name="name">メソッド名。</param>
            /// <param name="args">メソッドに渡す引数。</param>
            /// <param name="typeArgs">メソッドの型引数。</param>
            /// <param name="paramTypes">引数の型。</param>
            /// <returns>指定した条件に一致するメソッド。</returns>
            private MethodInfo MatchMethod(string name, object[] args, Type[] typeArgs, Type[] paramTypes)
            {
                var nameMatched = this.type.GetMethods(TransparentFlags)
                    .Where(mi => mi.Name == name)
                    .ToArray();
                if (!nameMatched.Any())
                    throw new ArgumentException(string.Format("\"{0}\" is not found : Type <{1}>", name, this.type.Name));

                var typedMethods = nameMatched
                    .Select(mi =>
                    {
                        var genericArguments = mi.GetGenericArguments();

                        if (!typeArgs.Any() && !genericArguments.Any())
                        {
                            return new
                            {
                                MethodInfo = mi,
                                TypeParameters = default(Dictionary<Type, Type>)
                            };
                        }
                        else if (!typeArgs.Any())
                        {
                            var paramGenericTypes = mi.GetParameters()
                                .Select(pi => pi.ParameterType)
                                .Zip(paramTypes, Tuple.Create)
                                .GroupBy(a => a.Item1, a => a.Item2)
                                .Where(g => g.Key.IsGenericParameter)
                                .Select(g => new { g.Key, Type = g.Aggregate(AssignableBoundType) })
                                .Where(a => a.Type != null);

                            var typeParams = genericArguments
                                .GroupJoin(paramGenericTypes, x => x, x => x.Key, (_, Args) => Args)
                                .ToArray();

                            if (!typeParams.All(xs => xs.Any()))
                                return null;

                            return new
                            {
                                MethodInfo = mi,
                                TypeParameters = typeParams
                                    .Select(xs => xs.First())
                                    .ToDictionary(a => a.Key, a => a.Type)
                            };
                        }
                        else
                        {
                            if (genericArguments.Length != typeArgs.Length)
                                return null;

                            return new
                            {
                                MethodInfo = mi,
                                TypeParameters = genericArguments
                                    .Zip(typeArgs, Tuple.Create)
                                    .ToDictionary(t => t.Item1, t => t.Item2)
                            };
                        }
                    })
                    .Where(a => a != null)
                    //.Where(a => a.MethodInfo
                    //    .GetParameters()
                    //    .Select(pi => pi.ParameterType)
                    //    .SequenceEqual(paramTypes, new EqualsComparer<Type>((x, y) =>
                    //        (x.IsGenericParameter)
                    //            ? a.TypeParameters[x].IsAssignableFrom(y)
                    //            : x.Equals(y)))
                    //)
                    .Where(a =>
                    {
                        var parameters = a.MethodInfo.GetParameters();
                        if (parameters.Length > 0 && parameters.Last().IsDefined(typeof(ParamArrayAttribute), false))
                        {
                            var types = parameters
                                .Take(parameters.Count() - 1)
                                .Select(pi => pi.ParameterType)
                                .Concat(Enumerable.Repeat(parameters.Last().ParameterType.GetElementType(), paramTypes.Length - (parameters.Length - 1)));
                            return types
                                .SequenceEqual(paramTypes, new EqualsComparer<Type>((x, y) =>
                                    (x.IsGenericParameter)
                                        ? a.TypeParameters[x].IsAssignableFrom(y)
                                        : x.IsAssignableFrom(y)));
                        }
                        else
                        {
                            return parameters
                                .Select(pi => pi.ParameterType)
                                .SequenceEqual(paramTypes, new EqualsComparer<Type>((x, y) =>
                                    (x.IsGenericParameter)
                                        ? a.TypeParameters[x].IsAssignableFrom(y)
                                        : x.IsAssignableFrom(y)));
                        }
                    })
                    .ToArray();

                if (!typedMethods.Any())
                    throw new ArgumentException(string.Format("\"{0}\" not match arguments : Type <{1}>", name, this.type.Name));

                //var nonGeneric = typedMethods.Where(a => a.TypeParameters == null).ToArray();
                //if (nonGeneric.Length == 1)
                //    return nonGeneric[0].MethodInfo;
                var nonGenerics = typedMethods.Where(a => a.TypeParameters == null);
                var nonGeneric = nonGenerics.Where(m =>
                    m.MethodInfo.GetParameters().Length == args.Length &&
                    (m.MethodInfo.GetParameters().Length > 0) ?
                    !m.MethodInfo.GetParameters().Last().IsDefined(typeof(ParamArrayAttribute), false) :
                    true).ToArray();
                if (nonGeneric.Length == 1)
                    return nonGeneric[0].MethodInfo;

                nonGeneric = nonGenerics.Where(m =>
                    m.MethodInfo.GetParameters().Length > 0 &&
                    m.MethodInfo.GetParameters().Last().IsDefined(typeof(ParamArrayAttribute), false)
                    ).ToArray();
                if (nonGeneric.Length == 1)
                    return nonGeneric[0].MethodInfo;

                var lessGeneric = typedMethods
                    .Where(a => !a.MethodInfo.GetParameters().All(pi => pi.ParameterType.IsGenericParameter))
                    .ToArray();

                var generic = (typedMethods.Length == 1)
                    ? typedMethods[0]
                    : (lessGeneric.Length == 1) ? lessGeneric[0] : null;

                if (generic != null)
                    return generic.MethodInfo.MakeGenericMethod(generic.TypeParameters.Select(kvp => kvp.Value).ToArray());

                throw new ArgumentException(string.Format("\"{0}\" ambiguous arguments : Type <{1}>", name, this.type.Name));
            }

            /// <summary>
            /// ２つの型のうち、継承関係で上位にある型を取得する。
            /// </summary>
            /// <param name="left">比較対象の型。</param>
            /// <param name="right">比較対象の型。</param>
            /// <returns>継承関係で上位にある型。２つの型に互換性がない場合は null。</returns>
            private Type AssignableBoundType(Type left, Type right)
            {
                return (left == null || right == null) ? null
                    : left.IsAssignableFrom(right) ? left
                    : right.IsAssignableFrom(left) ? right
                    : null;
            }

            #region EqualsComparer
            /// <summary>
            /// オブジェクトの比較に使用するデリゲートを外部から与える
            /// <see cref="System.Collections.Generic.IEqualityComparer{T}"/> 実装。
            /// </summary>
            /// <typeparam name="TX">比較するオブジェクトの型。</typeparam>
            private class EqualsComparer<TX> : IEqualityComparer<TX>
            {
                /// <summary>
                /// オブジェクトの比較に使用するデリゲート。
                /// </summary>
                private readonly Func<TX, TX, bool> equals;

                /// <summary>
                /// <see cref="EqualsComparer{T}"/> の新しいインスタンスを生成する。
                /// </summary>
                /// <param name="equals">オブジェクトの比較に使用するデリゲート。</param>
                public EqualsComparer(Func<TX, TX, bool> equals)
                {
                    this.equals = equals;
                }

                /// <summary>
                /// 指定したオブジェクトが等しいかどうかを判断する。
                /// </summary>
                /// <param name="x">比較対象のオブジェクト。</param>
                /// <param name="y">比較対象のオブジェクト。</param>
                /// <returns>オブジェクトが等しい場合は true、そうでない場合は false。</returns>
                public bool Equals(TX x, TX y)
                {
                    return this.equals(x, y);
                }

                /// <summary>
                /// 指定したオブジェクトのハッシュコードを返す。
                /// </summary>
                /// <param name="obj">対象のオブジェクト。</param>
                /// <returns>指定したオブジェクトのハッシュコード。</returns>
                public int GetHashCode(TX obj)
                {
                    return 0;
                }
            }
            #endregion

            #endregion

            #endregion
        }
        #endregion

        #endregion
    }
    #endregion
}
